package app

import (
	"fmt"
	"net"
	"net/http"
	"os"
	"runtime"
	"strings"
	"time"

	"rank/internal/api"
	"rank/internal/store"
	"rank/pkg/middleware"

	zlog "github.com/asim/go-micro/plugins/logger/zerolog/v3"
	"github.com/asim/go-micro/v3/config"
	"github.com/asim/go-micro/v3/config/source/env"
	"github.com/asim/go-micro/v3/config/source/flag"
	"github.com/asim/go-micro/v3/logger"
	"github.com/duke-git/lancet/netutil"
	"github.com/felixge/fgprof"
	"github.com/gin-contrib/cors"
	"github.com/gin-contrib/pprof"
	"github.com/gin-gonic/gin"
	"github.com/google/gops/agent"
	"github.com/pkg/errors"
	"github.com/redis/go-redis/v9"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

const (
	timeFormat = "2006-01-02 15:04:05.000"
)

// Option ...
type Option func(*app) error

// Config ...
func Config() Option {
	return func(a *app) (err error) {
		flagSource := flag.NewSource(
			flag.IncludeUnset(true),
		)

		a.conf, err = config.NewConfig()
		if err != nil {
			return errors.Wrap(err, "Config()")
		}

		err = a.conf.Load(flagSource, env.NewSource())
		if err != nil {
			return err
		}

		return nil
	}
}

// Version ...
func Version(versionInfo string) Option {
	return func(a *app) (err error) {
		printVersion := a.conf.Get(printVersionKey).Bool(printVersionDef)
		if printVersion {
			_, _ = fmt.Fprintf(os.Stderr, "%v build info: %v\n", serverName,
				strings.ReplaceAll(versionInfo, "_", "\n"))
			os.Exit(0)
		}

		return nil
	}
}

// Logger ...
func Logger() Option {
	return func(a *app) error {
		lv := a.conf.Get(logLevelKey).String(logLevelDef)
		level, err := zerolog.ParseLevel(lv)
		if err != nil {
			level = zerolog.DebugLevel
		}

		zerolog.TimestampFieldName = "ts"
		zerolog.MessageFieldName = "msg"
		zerolog.LevelFieldName = "lvl"
		zerolog.TimeFieldFormat = timeFormat

		simpleHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
			if _, file, line, ok := runtime.Caller(4); ok {
				// 取文件名
				idx := strings.LastIndexByte(file, '/')
				if idx == -1 {
					e.Str("file", fmt.Sprintf("%s:%d", file, line))
					return
				}

				// 取包名
				idx = strings.LastIndexByte(file[:idx], '/')
				if idx == -1 {
					e.Str("file", fmt.Sprintf("%s:%d", file[:idx], line))
					return
				}

				// 返回包名和文件名
				e.Str("file", fmt.Sprintf("%s:%d", file[idx+1:], line))
			}
		})

		ip := netutil.GetInternalIp()
		log.Logger = zerolog.New(os.Stdout).Level(level).Hook(simpleHook).With().Timestamp().
			IPAddr("ip", net.ParseIP(ip)).Logger()
		log.Info().Msg("Init logger successfully.")

		loglevel, _ := logger.GetLevel(lv)
		logger.DefaultLogger = zlog.NewLogger(
			logger.WithOutput(os.Stdout),
			logger.WithLevel(loglevel),
			zlog.WithTimeFormat(timeFormat),
			zlog.WithProductionMode(),
			zlog.WithHooks([]zerolog.Hook{simpleHook}),
			logger.WithFields(map[string]interface{}{"ip": ip}),
		)
		return nil
	}
}

// RedisCli ...
func RedisCli() Option {
	return func(a *app) error {
		conf := &redisConf{
			Mode:        redisModeStandalone,
			Addr:        "127.0.0.1:6379",
			Password:    "",
			MaxRetries:  3,
			PoolSize:    100,
			MinIdleConn: 10,
			MaxConnAge:  2, // Hour
			IdleTimeout: 5, // Minute
		}

		if conf.Mode == redisModeCluster {
			a.redisCli = redis.NewClusterClient(&redis.ClusterOptions{
				Addrs:           []string{conf.Addr},
				Password:        conf.Password,
				MaxRetries:      conf.MaxRetries,
				PoolSize:        conf.PoolSize,
				MinIdleConns:    conf.MinIdleConn,
				ConnMaxLifetime: time.Duration(conf.MaxConnAge) * time.Hour,
				ConnMaxIdleTime: time.Duration(conf.IdleTimeout) * time.Minute,
			})
		} else {
			a.redisCli = redis.NewClient(&redis.Options{
				Addr:            conf.Addr,
				Password:        conf.Password,
				MaxRetries:      conf.MaxRetries,
				PoolSize:        conf.PoolSize,
				MinIdleConns:    conf.MinIdleConn,
				ConnMaxLifetime: time.Duration(conf.MaxConnAge) * time.Hour,
				ConnMaxIdleTime: time.Duration(conf.IdleTimeout) * time.Minute,
			})
		}

		if a.redisCli == nil {
			return errors.Errorf("create redis client failed, %v", conf)
		}

		log.Info().Msg("New Redis client successfully.")
		return nil
	}
}

// Dao ...
func Dao() Option {
	return func(a *app) (err error) {
		a.dao = store.NewDao(a.redisCli)
		if a.dao == nil {
			return errors.New("create dao failed")
		}

		if err := a.dao.Init(); err != nil {
			return errors.Wrap(err, "Init dao failed")
		}

		log.Info().Msg("New dao successfully.")
		return
	}
}

// WebService ...
func WebService() Option {
	return func(a *app) error {
		conf := &webConf{
			GinMode:  gin.ReleaseMode,
			Port:     8080,
			LimitRPS: 2000,
		}

		// 创建路由
		gin.SetMode(conf.GinMode)

		if !gin.IsDebugging() {
			a.ginRouter = gin.New()
			if gin.Mode() == gin.TestMode {
				a.ginRouter.Use(gin.Logger())
			}
			a.ginRouter.Use(gin.Recovery())
		} else {
			a.ginRouter = gin.Default()
		}
		a.ginRouter.Use(cors.Default())
		a.ginRouter.Use(middleware.NewRateLimiter(time.Second, int64(conf.LimitRPS)))
		a.ginRouter.NoRoute(func(ctx *gin.Context) {
			ctx.AbortWithStatus(http.StatusNotFound)
		})

		// analyze On-CPU as well as Off-CPU time
		a.ginRouter.GET("/debug/fgprof", gin.WrapH(fgprof.Handler()))

		// pprof
		pprof.Register(a.ginRouter)

		// 配置 swagger address
		ip := netutil.GetInternalIp()
		swaggerAddr := fmt.Sprintf("%v:%v", ip, conf.Port)

		// 构建 web handler
		handler := api.NewRestHandler(a.dao, swaggerAddr)
		if err := handler.Init(serverName); err != nil {
			return errors.Wrap(err, "init WebService handler")
		}

		if err := handler.RegisterHandler(a.ginRouter); err != nil {
			return errors.Wrap(err, "option WebService")
		}

		if err := agent.Listen(agent.Options{
			Addr:            fmt.Sprintf(":%v", conf.Port+20000),
			ShutdownCleanup: true,
		}); err != nil {
			log.Err(err).Msg("gops agent listen failed")
		}

		log.Info().Msg("New web service successfully.")
		return nil
	}
}
